/*
   実行環境は
   VMware Fusion で動作する ubuntu 12.04 64bit

   # コンパイルは
   src="my_viewer"
   opt=$( pkg-config gtk+-2.0 --cflags --libs )
   g++ -o $src $src.cpp $opt

   # 実行は
   img="lenna.png"
   ./$src $img

   os X から ssh -X 使用時 表示をホストに変えるときは
   export DISPLAY=:0.0
   os X に戻すときは
   export DISPLAY=:10.0
*/

#include <iostream>
#include <gtk/gtk.h>

  #define my_signal_connect( object , signal , callback ) ( \
    g_signal_connect( G_OBJECT( object ), ( signal ), G_CALLBACK( callback ), data ) \
  )

  int navi_setup(
    gint         req_width ,
    gpointer   * data
  ) {
    int          argc  = *( (int         *) data[0] ) ;
    char      ** argv  = *( (char      ***) data[1] ) ;
    GdkPixbuf ** image =    (GdkPixbuf  **) data[2]   ; // navi_image

    GdkPixbuf * src_img ;
      std::string file_name ;
        if( argc < 2 ) { g_print( "%s: no file name\n", argv[0] ) ; return -1 ; }
        file_name = argv[1] ;
      src_img = gdk_pixbuf_new_from_file( file_name .c_str() , NULL );
        if( ! src_img ) { g_print( "%s: file cannot read\n", argv[0] ); return -1 ; }
    GtkAllocation src_size ;
      src_size .width  = gdk_pixbuf_get_width(  src_img );
      src_size .height = gdk_pixbuf_get_height( src_img );
    gdouble image_k ;
      image_k = (gdouble) req_width / src_size .width ;
      if( 1.0 < image_k ) { image_k = 1.0 ; }
    GtkAllocation image_size ;
      image_size .width  = int( image_k * src_size .width  + 0.5 );
      image_size .height = int( image_k * src_size .height + 0.5 );
    // update image
      if( *image != NULL ) { g_object_unref( *image ); }
      *image = gdk_pixbuf_scale_simple(
        src_img , image_size .width , image_size .height , GDK_INTERP_BILINEAR
      );
    g_object_unref( src_img );
    return 0 ;
  }

  int expand_setup(
    gpointer   * data
  ) {
    int          argc  = *( (int         *) data[0] ) ;
    char      ** argv  = *( (char      ***) data[1] ) ;
    GdkPixbuf ** image =   (GdkPixbuf  **) data[3]   ; // expand_image

    GdkPixbuf * src_img ;
      std::string file_name ;
        if( argc < 2 ) { g_print( "%s: no file name\n", argv[0] ) ; return -1 ; }
        file_name = argv[1] ;
      src_img = gdk_pixbuf_new_from_file( file_name .c_str() , NULL );
        if( ! src_img ) { g_print( "%s: file cannot read\n", argv[0] ); return -1 ; }
    // update image
      *image = src_img ;
    return 0 ;
  }

  gboolean navi_expose(
    GtkWidget      * widget,
    GdkEventExpose * event,
    gpointer       * data
  ) {
    GdkPixbuf    ** image  = (GdkPixbuf    **) data[2] ; // navi_image
    GdkPixbuf    ** expand = (GdkPixbuf    **) data[3] ; // expand_image
    GdkRectangle  * roi    = (GdkRectangle  *) data[4] ;

    cairo_t * cr ;
      gtk_widget_set_size_request(
        widget , gdk_pixbuf_get_width( *image ) , gdk_pixbuf_get_height( *image )
      );
      cr = gdk_cairo_create( widget -> window );
    // draw image
      gdk_cairo_set_source_pixbuf( cr , *image , 0.0 , 0.0 );
      cairo_paint( cr );
    gdouble k ;
      k = (gdouble) gdk_pixbuf_get_width( *image ) / gdk_pixbuf_get_width( *expand ) ;
    // draw roi
      cairo_set_source_rgb( cr , 0.0 , 1.0 , 0.0 );
      cairo_rectangle(
        cr , k * roi -> x , k * roi -> y , k * roi -> width , k * roi -> height
      );
      cairo_stroke( cr );
    cairo_destroy( cr );
    return FALSE ;
  }

  void navi_roi_scroll(
    gpointer * data
  ) {
    GdkPixbuf    ** image  = (GdkPixbuf    **) data[2] ; // navi_image
    GdkPixbuf    ** expand = (GdkPixbuf    **) data[3] ; // expand_image
    GdkRectangle  * roi    = (GdkRectangle  *) data[4] ;
    GtkWidget     * paned  = (GtkWidget     *) data[6] ; // navi_paned

    GtkAdjustment * h_adjust ; GtkAdjustment * v_adjust ;
      h_adjust = gtk_scrolled_window_get_hadjustment( GTK_SCROLLED_WINDOW( paned ) );
      v_adjust = gtk_scrolled_window_get_vadjustment( GTK_SCROLLED_WINDOW( paned ) );
    GtkAllocation pane_size ;
      gtk_widget_get_allocation( paned , & pane_size );
    gdouble h_val ; gdouble v_val ;
      h_val = gtk_adjustment_get_value( h_adjust );
      v_val = gtk_adjustment_get_value( v_adjust );
    gdouble k ;
      k = (gdouble) gdk_pixbuf_get_width( *image )  / gdk_pixbuf_get_width( *expand ) ;
    gdouble h_new ; gdouble v_new ;
      h_new = h_val ;
      v_new = v_val ;
      if( k * ( roi -> x + roi -> width ) > h_val + pane_size .width ) {
        h_new = k * ( roi -> x + roi -> width ) - pane_size .width + 4 ;
      }
      if( k * ( roi -> y + roi -> height ) > v_val + pane_size .height ) {
        v_new = k * (roi -> y + roi -> height ) - pane_size .height + 5 ;
      }
      if( k * roi -> x < h_val ) { h_new = k * roi -> x ; }
      if( k * roi -> y < v_val ) { v_new = k * roi -> y ; }
    // move page top
      if( h_val != h_new ) { gtk_adjustment_set_value( h_adjust , h_new ); }
      if( v_val != v_new ) { gtk_adjustment_set_value( v_adjust , v_new ); }
  }

  gboolean navi_v_changed(
    GtkWidget   * widget ,
    gpointer    * data
  ) {
    navi_roi_scroll( data );
    return FALSE ;
  }

  void expand_update(
    gpointer * data
  ) {
    GdkRectangle  * roi   = (GdkRectangle  *) data[4] ;
    GtkWidget     * paned = (GtkWidget     *) data[7] ; // expand_paned

    GtkAdjustment * h_adjust ; GtkAdjustment * v_adjust ;
      h_adjust = gtk_scrolled_window_get_hadjustment( GTK_SCROLLED_WINDOW( paned ) );
      v_adjust = gtk_scrolled_window_get_vadjustment( GTK_SCROLLED_WINDOW( paned ) );
    gdouble h_val ; gdouble v_val ;
      h_val = gtk_adjustment_get_value( h_adjust );
      v_val = gtk_adjustment_get_value( v_adjust );
    gdouble h_new ; gdouble v_new ;
      h_new = roi -> x ;
      v_new = roi -> y ;
    // move page top
      if( h_val != h_new ) { gtk_adjustment_set_value( h_adjust , h_new ); }
      if( v_val != v_new ) { gtk_adjustment_set_value( v_adjust , v_new ); }
  }

  gboolean expand_changed(
    GtkWidget   * widget ,
    gpointer    * data
  ) {
    GdkRectangle  * roi    = (GdkRectangle  *) data[4] ;
    GtkWidget     * canvas = (GtkWidget     *) data[5] ; // navi_canvas
    GtkWidget     * paned  = (GtkWidget     *) data[7] ; // expand_paned

    GtkAdjustment * h_adjust ; GtkAdjustment * v_adjust ;
      h_adjust = gtk_scrolled_window_get_hadjustment( GTK_SCROLLED_WINDOW( paned ) );
      v_adjust = gtk_scrolled_window_get_vadjustment( GTK_SCROLLED_WINDOW( paned ) );
    GtkAllocation pane_size ;
      gtk_widget_get_allocation( paned , & pane_size );
    gdouble h_val ; gdouble v_val ;
      h_val = gtk_adjustment_get_value( h_adjust );
      v_val = gtk_adjustment_get_value( v_adjust );
    // update roi
      roi -> x = h_val ;
      roi -> y = v_val ;
      roi -> width  = pane_size .width ;
      roi -> height = pane_size .height ;
    // update navi
      navi_roi_scroll( data );
      gtk_widget_queue_draw( canvas );
    return FALSE ;
  }

  void navi_scale_callback(
    GtkWidget  * button ,
    gpointer   * data
  ) {
    GdkPixbuf    ** image  = (GdkPixbuf    **) data[2] ; // navi_image
    GdkPixbuf    ** expand = (GdkPixbuf    **) data[3] ; // expand_image
    GtkWidget     * canvas = (GtkWidget     *) data[5] ; // navi_canvas
    GtkWidget     * paned  = (GtkWidget     *) data[6] ; // navi_paned

    GtkAllocation image_size ;
      image_size .width  = gdk_pixbuf_get_width(  *image );
      image_size .height = gdk_pixbuf_get_height( *image );
    GtkAllocation expand_size ;
      expand_size .width  = gdk_pixbuf_get_width(  *expand );
      expand_size .height = gdk_pixbuf_get_height( *expand );
    GtkAllocation pane_size ;
      gtk_widget_get_allocation( paned , & pane_size );
    gint req_width ;
      std::string label ;
        label = gtk_button_get_label( GTK_BUTTON( button ) ) ;
      if( label .compare( "⇔" ) == 0 ) { req_width = pane_size .width - 4 ; }
      if( label .compare( "⇕" ) == 0 ) {
        double k ;
          k = (double) ( pane_size .height - 5 ) / image_size .height ;
        req_width = int( k * image_size .width + 0.5 );
      }
      if( label .compare( "＋" ) == 0 ) { req_width = image_size .width + 150 ; }
      if( label .compare( "ー" ) == 0 ) { req_width = image_size .width - 150 ; }
      if( req_width < 500  ) { req_width = 500 - 4 ; }
      if( req_width > 2000 ) { req_width = 2000 ; }
      if( req_width > expand_size .width ) { req_width = expand_size .width ; }
    // update image , navi
      navi_roi_scroll( data );
      if( navi_setup( req_width , data ) < 0 ) { return ; }
      gtk_widget_queue_draw( canvas );
    // update detail
      expand_update( data );
  }

  gboolean key_pressed(
    GtkWidget   * widget ,
    GdkEventKey * event ,
    gpointer    * data
  ) {
    GdkPixbuf    ** image  = (GdkPixbuf    **) data[3] ; // expand_image
    GdkRectangle  * roi    = (GdkRectangle  *) data[4] ;
    GtkWidget     * canvas = (GtkWidget     *) data[5] ; // navi_canvas

    GtkAllocation image_size ;
      image_size .width  = gdk_pixbuf_get_width(  *image );
      image_size .height = gdk_pixbuf_get_height( *image );
    GtkAllocation roi_limit ;
      roi_limit .width  = image_size .width  - roi -> width ;
      roi_limit .height = image_size .height - roi -> height ;
    // adjust roi
      guint shift ;
        shift = event -> state & GDK_SHIFT_MASK ;
      switch( (guint) event -> keyval ) {
        case 0xff51 : // ←
          roi -> x = shift ? 0 : roi -> x - roi -> width ; break ;
        case 0xff52 : // ↑
          roi -> y = shift ? 0 : roi -> y - roi -> height ; break ;
        case 0xff53 : // →
          roi -> x = shift ? roi_limit .width  : roi -> x + roi -> width ;  break ;
        case 0xff54 : // ↓
          roi -> y = shift ? roi_limit .height : roi -> y + roi -> height ; break ;
        default :
          // g_print( "%x\n", event -> keyval );
          return FALSE ;
      }
      if( roi -> x < 0 ) { roi -> x = 0 ; }
      if( roi -> y < 0 ) { roi -> y = 0 ; }
      if( roi -> x > roi_limit .width )  { roi -> x = roi_limit .width ; }
      if( roi -> y > roi_limit .height ) { roi -> y = roi_limit .height ; }
    // update navi
      navi_roi_scroll( data );
      gtk_widget_queue_draw( canvas );
    // update detail
      expand_update( data );
    return TRUE ;
  }

int main(
  int     argc ,
  char ** argv
) {

  gtk_init( & argc , & argv );

  gpointer data[9] ; {
    data[0] = & argc ;
    data[1] = & argv ;
    data[2] = NULL ; // for & navi_image ;
    data[3] = NULL ; // for & expand_image ;
    data[4] = NULL ; // for & roi ;
    data[5] = NULL ; // for navi_canvas ;
    data[6] = NULL ; // for navi_paned ;
    data[7] = NULL ; // for expand_paned ;
  }

  GdkPixbuf * navi_image ; {
    data[2] = & navi_image ;  navi_image  = NULL ;
    if( navi_setup( 1200 , data ) < 0 ) { return -1 ; }
  }

  GdkPixbuf * expand_image ; {
    data[3] = & expand_image ; expand_image = NULL ;
    if( expand_setup( data ) < 0 ) { return -1 ; }
  }

  GdkRectangle roi ; {
    GtkAllocation expand_size ;
      expand_size .width  = gdk_pixbuf_get_width(  expand_image );
      expand_size .height = gdk_pixbuf_get_height( expand_image );
    roi .x = 0 ;
    roi .y = 0 ;
    roi .width  = int( (double) expand_size .width  / 3 + 0.5 );
    roi .height = int( (double) expand_size .height / 5 + 0.5 );
    data[4] = & roi ;
  }

  GtkWidget * navi_window ; {
    int width ; int height ;
      width = 510 ; height = 750 ;
    navi_window = gtk_window_new( GTK_WINDOW_TOPLEVEL );
      gtk_widget_set_size_request( navi_window , width , height );
      g_signal_connect( G_OBJECT( navi_window ), "destroy", G_CALLBACK( gtk_main_quit ), NULL );
  }

  GtkWidget * navi_vbox ; {
    navi_vbox = gtk_vbox_new( FALSE , 2 );
    gtk_container_add( GTK_CONTAINER ( navi_window ) , navi_vbox );
  }

  GtkWidget * navi_canvas ; {
    navi_canvas = gtk_drawing_area_new( );
      my_signal_connect( navi_canvas , "expose_event", navi_expose );
    data[5] = navi_canvas ;
  }

  GtkWidget * navi_paned ; {
    navi_paned = gtk_scrolled_window_new( NULL , NULL );
      gtk_scrolled_window_set_policy(
        GTK_SCROLLED_WINDOW( navi_paned ), GTK_POLICY_AUTOMATIC , GTK_POLICY_AUTOMATIC
      );
      gtk_scrolled_window_add_with_viewport( GTK_SCROLLED_WINDOW( navi_paned ) , navi_canvas );
      my_signal_connect( navi_paned , "key_press_event", key_pressed );
    gtk_box_pack_start( GTK_BOX (navi_vbox) , navi_paned , TRUE , TRUE , 0 );
    data[6] = navi_paned ;

    GtkAdjustment * v_adjust ;
      v_adjust = gtk_scrolled_window_get_vadjustment( GTK_SCROLLED_WINDOW( navi_paned ) );
      my_signal_connect( v_adjust , "changed", navi_v_changed );
  }

  GtkWidget * navi_hbox ; {
    navi_hbox = gtk_hbox_new( FALSE, 0 );
    gtk_box_pack_start( GTK_BOX (navi_vbox) , navi_hbox , FALSE , FALSE , 2 );
  }

  GtkWidget * navi_scale ; {
    navi_scale = gtk_hbox_new( FALSE, 0 );
    gtk_box_pack_start( GTK_BOX (navi_hbox) , navi_scale , FALSE , FALSE , 2 );

    GtkWidget* scale_button[4] ;
      #define navi_scale_add( col , label ) ( \
        scale_button[ col ] = gtk_button_new_with_label( label ) , \
        gtk_box_pack_start( GTK_BOX( navi_scale ), scale_button[ col ], FALSE, FALSE, 0 ) , \
        my_signal_connect( scale_button[ col ], "clicked", navi_scale_callback ) \
      )
    navi_scale_add( 0, "＋" ) ;
    navi_scale_add( 1, "ー" ) ;
    navi_scale_add( 2, "⇕" ) ;
    navi_scale_add( 3, "⇔" ) ;
  }

  GtkWidget * expand_window ; {
    expand_window = gtk_window_new( GTK_WINDOW_TOPLEVEL );
      gtk_window_set_title( GTK_WINDOW( expand_window ), "detail" );
      gtk_window_set_default_size( GTK_WINDOW( expand_window ), roi .width , roi .height );
    gint navi_window_width ;
      gtk_window_get_size( GTK_WINDOW( navi_window ) , & navi_window_width , NULL );
    gtk_window_move( GTK_WINDOW( expand_window ), navi_window_width + 30 , 0 );
    gtk_window_iconify( GTK_WINDOW( expand_window ) );
  }

  GtkWidget * expand_canvas ; {
    expand_canvas = gtk_image_new_from_pixbuf( expand_image );
  }

  GtkWidget * expand_paned ; {
    expand_paned = gtk_scrolled_window_new( NULL , NULL );
      gtk_scrolled_window_set_policy(
        GTK_SCROLLED_WINDOW( expand_paned ), GTK_POLICY_AUTOMATIC , GTK_POLICY_AUTOMATIC
      );
      gtk_scrolled_window_add_with_viewport( GTK_SCROLLED_WINDOW( expand_paned ) , expand_canvas );
    gtk_container_add( GTK_CONTAINER( expand_window ) , expand_paned );
    data[7] = expand_paned ;

    GtkAdjustment * h_adjust ;
      h_adjust = gtk_scrolled_window_get_hadjustment( GTK_SCROLLED_WINDOW( expand_paned ) );
      my_signal_connect( h_adjust , "value-changed" , expand_changed );
      my_signal_connect( h_adjust , "changed" ,       expand_changed );
    GtkAdjustment * v_adjust ;
      v_adjust = gtk_scrolled_window_get_vadjustment( GTK_SCROLLED_WINDOW( expand_paned ) );
      my_signal_connect( v_adjust , "value-changed" , expand_changed );
      my_signal_connect( v_adjust , "changed" ,       expand_changed );
  }

  gtk_widget_show_all( navi_window );
  gtk_widget_show_all( expand_window );
  gtk_main( );

  g_object_unref( navi_image );
  g_object_unref( expand_image );
  return 0 ;
}

